// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library pub.lock_file;

import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';

import 'io.dart';
import 'package.dart';
import 'source_registry.dart';
import 'utils.dart';

/// A parsed and validated `pubspec.lock` file.
class LockFile {
  /// The packages this lockfile pins.
  Map<String, PackageId> packages;

  /// Creates a new lockfile containing [ids].
  factory LockFile(List<PackageId> ids) {
    var lockFile = new LockFile.empty();
    for (var id in ids) {
      if (!id.isRoot) lockFile.packages[id.name] = id;
    }

    return lockFile;
  }

  LockFile._(this.packages);

  LockFile.empty()
    : packages = <String, PackageId>{};

  /// Loads a lockfile from [filePath].
  factory LockFile.load(String filePath, SourceRegistry sources) {
    return LockFile._parse(filePath, readTextFile(filePath), sources);
  }

  /// Parses a lockfile whose text is [contents].
  factory LockFile.parse(String contents, SourceRegistry sources) {
    return LockFile._parse(null, contents, sources);
  }

  /// Parses the lockfile whose text is [contents].
  ///
  /// [filePath] is the system-native path to the lockfile on disc. It may be
  /// `null`.
  static LockFile _parse(String filePath, String contents,
      SourceRegistry sources) {
    var packages = <String, PackageId>{};

    if (contents.trim() == '') return new LockFile.empty();

    var sourceUrl;
    if (filePath != null) sourceUrl = p.toUri(filePath);
    var parsed = loadYamlNode(contents, sourceUrl: sourceUrl);

    _validate(parsed is Map, 'The lockfile must be a YAML mapping.', parsed);

    var packageEntries = parsed['packages'];
    if (packageEntries != null) {
      _validate(packageEntries is Map, 'The "packages" field must be a map.',
          parsed.nodes['packages']);

      packageEntries.forEach((name, spec) {
        // Parse the version.
        _validate(spec.containsKey('version'),
            'Package $name is missing a version.', spec);
        var version = new Version.parse(spec['version']);

        // Parse the source.
        _validate(spec.containsKey('source'),
            'Package $name is missing a source.', spec);
        var sourceName = spec['source'];

        _validate(spec.containsKey('description'),
            'Package $name is missing a description.', spec);
        var description = spec['description'];

        // Let the source parse the description.
        var source = sources[sourceName];
        try {
          description = source.parseDescription(filePath, description,
              fromLockFile: true);
        } on FormatException catch (ex) {
          throw new SourceSpanFormatException(ex.message,
              spec.nodes['source'].span);
        }

        var id = new PackageId(name, sourceName, version, description);

        // Validate the name.
        _validate(name == id.name,
            "Package name $name doesn't match ${id.name}.", spec);

        packages[name] = id;
      });
    }

    return new LockFile._(packages);
  }

  /// If [condition] is `false` throws a format error with [message] for [node].
  static void _validate(bool condition, String message, YamlNode node) {
    if (condition) return;
    throw new SourceSpanFormatException(message, node.span);
  }

  /// Returns the serialized YAML text of the lock file.
  ///
  /// [packageDir] is the containing directory of the root package, used to
  /// properly serialize package descriptions.
  String serialize(String packageDir, SourceRegistry sources) {
    // Convert the dependencies to a simple object.
    var data = {};
    packages.forEach((name, package) {
      var description = sources[package.source].serializeDescription(packageDir,
          package.description);

      data[name] = {
        'version': package.version.toString(),
        'source': package.source,
        'description': description
      };
    });

    return """
# Generated by pub
# See http://pub.dartlang.org/doc/glossary.html#lockfile
${yamlToString({'packages': data})}
""";
  }
}
